package com.ibm.cps.sql.expression.function.meta;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bson.types.ObjectId;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ibm.cps.message.MessageFields;
import com.ibm.cps.sql.expression.ArrayVisitingNode;
import com.ibm.cps.sql.expression.BooleanLiteralNode;
import com.ibm.cps.sql.expression.ExpressionNode;
import com.ibm.cps.sql.expression.IdentifierNode;
import com.ibm.cps.sql.expression.NumberNode;
import com.ibm.cps.sql.expression.StringLiteralNode;
import com.ibm.util.ErrorCode;
import com.ibm.util.JSONUtility;
import com.ibm.util.exception.CPSException;

import akka.event.slf4j.Logger;

public class FunctionMetaCreator {
	public static final int INT_CONST = 0x1;
	public static final int DOUBLE_CONST = 0x4;
	public static final int NUMBER_CONST = INT_CONST | DOUBLE_CONST;
	public static final int STRING_CONST = 0x8;
	public static final int FIELD = 0x10;
	public static final int BOOLEAN_CONST = 0x20;

	private final Class<?> processorClass;

	public FunctionMetaCreator(Class<?> processorClass) {
		this.processorClass = processorClass;
	}

	public ObjectNode create(String functionName, String parentProcessorId, String groupId, String outputFieldName, ExpressionNode... arguments) throws CPSException{
		List<ArgDescriptor> argTypes = parseArguments(arguments);
		List<ArgChecker> validArgLists = findValidArgumentLists(true);
		ObjectNode metadata = createMetadata(functionName, parentProcessorId, groupId, argTypes, validArgLists, outputFieldName);
		return metadata;
	}

	private List<ArgDescriptor> parseArguments(ExpressionNode... arguments) throws CPSException{
		List<ArgDescriptor> args = new ArrayList<>();
		for (ExpressionNode node : arguments) {
			if(node instanceof NumberNode){
				NumberNode number = (NumberNode)node;
				double value = number.getValue();
				if(Math.round(value) == value){
					args.add(new ArgDescriptor(INT_CONST, (int)value));
				}else{
					args.add(new ArgDescriptor(DOUBLE_CONST, value));
				}
			}else if(node instanceof StringLiteralNode){
				String stringConst = ((StringLiteralNode)node).getValue();
				args.add(new ArgDescriptor(STRING_CONST, stringConst));
			}else if(node instanceof IdentifierNode){
				String fieldName = ((IdentifierNode)node).getName();
				args.add(new ArgDescriptor(FIELD, fieldName));
			}else if(node instanceof ArrayVisitingNode){
				String fieldName = ((ArrayVisitingNode)node).getValueFieldName();
				args.add(new ArgDescriptor(FIELD, fieldName));
			}else if(node instanceof BooleanLiteralNode){
				boolean boolConst = ((BooleanLiteralNode)node).getValue();
				args.add(new ArgDescriptor(BOOLEAN_CONST, boolConst));
			}else{
				throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Unexpected intermediate expression type " + node.getClass() + " in processing function node " + node.toString());
			}
		}
		return args;
	}

	private List<ArgChecker> findValidArgumentLists(boolean ignoreNonFunctionArgs) throws CPSException{
		List<ArgChecker> argumentLists = new ArrayList<>();
		Set<String> usedName = new HashSet<>();
		List<String> argNames = new ArrayList<>();
		List<String> fieldNames = new ArrayList<>();
		List<Integer> typeCodes = new ArrayList<>();
		Field[] fields = processorClass.getDeclaredFields();
		for (Field field : fields) {
			String name = field.getName();
			int typeCode = 0;
			boolean shouldIgnore = field.isAnnotationPresent(IgnoredInFunctionCall.class) && ignoreNonFunctionArgs;
			if(!shouldIgnore && field.isAnnotationPresent(FieldNameParam.class)){
				String alias = field.getAnnotation(FieldNameParam.class).value();
				name = alias.equals("") ? name : alias;
				typeCode = FIELD;
			}
			if(!shouldIgnore && field.isAnnotationPresent(ValueParam.class)){
				Class<?> fieldType = field.getType();
				int typeCodeFromFieldType = convertParamTypeToTypeCode(name, fieldType);
				String alias = field.getAnnotation(ValueParam.class).value();
				name = alias.equals("") ? name : alias;
				typeCode = typeCodeFromFieldType;
			}
			if(typeCode != 0){
				if(Modifier.isFinal(field.getModifiers())){
					throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Parameter field " + field + " in processor " + processorClass + " is set to final, param field is not allowed to be final");
				}
				if(usedName.contains(name)){
					throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Naming conflict in field name " + name + " of " + processorClass);
				}
				argNames.add(name);
				fieldNames.add(field.getName());
				usedName.add(name);
				usedName.add(field.getName());
				typeCodes.add(typeCode);
			}
		}
		argumentLists.add(new ArgChecker(argNames, fieldNames, typeCodes));
		return argumentLists;
	}

	private int convertParamTypeToTypeCode(String paramName, Class<?> paramType) throws CPSException{
		if(paramType.equals(Integer.class) || paramType.equals(int.class))
			return INT_CONST;
		if(paramType.equals(String.class))
			return STRING_CONST;
		if(paramType.equals(Double.class) || paramType.equals(double.class) || paramType.equals(Float.class) || paramType.equals(float.class))
			return INT_CONST | DOUBLE_CONST;
		if(paramType.equals(Boolean.class) || paramType.equals(boolean.class))
			return BOOLEAN_CONST;
		throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Unexpected param Type " + paramType + " of processor " + processorClass + " member " + paramName + ", only primitive types or String allowed");
	}

	private ObjectNode createMetadata(String functionName, String parentProcessorId, String groupId, List<ArgDescriptor> argTypes, List<ArgChecker> validArgLists, String outputFieldName) throws CPSException{
		for (ArgChecker argChecker : validArgLists) {
			if(argChecker.accept(argTypes)){
				ObjectNode metadata = JSONUtility.newObjectNode();
				String processorId = new BigInteger(new ObjectId().toByteArray()).toString(16);
				metadata.put(MessageFields.PROCESSORID, processorId);
				metadata.put(MessageFields.PARENTPROCESSORS, parentProcessorId);
				metadata.put(MessageFields.GROUPID, groupId);
				metadata.put(MessageFields.TYPE, functionName);
				//TODO : to change the hard code here.
				metadata.put(MessageFields.OUTPUT_FIELD_NAME, outputFieldName);
				metadata.put("outputField", outputFieldName);
				for (int i = 0; i < argChecker.argNames.size(); i++) {
					String argName = argChecker.argNames.get(i);
					Object value = argTypes.get(i).value;
					metadata.put(argName, value.toString());
				}
				return metadata;
			}
		}
		throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Cannot find proper metadata creator for " + argTypes + " for processor " + processorClass);
	}

	public void assignProcessorAttributes(Object processor, Map<String, String> attributes) throws CPSException{
		List<ArgChecker> validArgLists = findValidArgumentLists(false);
		for (ArgChecker argChecker : validArgLists) {
			if(matchesInput(argChecker, attributes)){
				assignAttributes(processor, argChecker, attributes);
				return;
			}
		}
		throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Failed to assign value for " + processorClass + " , no proper arg list found, input args: " + attributes);
	}

	private boolean matchesInput(ArgChecker argChecker, Map<String, String> attributes){
		List<String> argNames = argChecker.argNames;
		for (String name : argNames) {
			if(!attributes.containsKey(name)){
				System.out.println("we are missing " + name);
				return false;
			}
				
		}
		return true;
	}

	private void assignAttributes(Object processor, ArgChecker argList, Map<String, String> attributes) throws CPSException{
		List<String> argNames = argList.argNames;
		List<String> fieldNames = argList.fieldNames;
		List<Integer> typeCodes = argList.argTypeCodes;
		try {
			for (int i = 0; i < argNames.size(); i++) {
				String name = argNames.get(i);
				String fieldName = fieldNames.get(i);
				int code = typeCodes.get(i);
				String valueString = attributes.get(name);
				Field field = processorClass.getDeclaredField(fieldName);
				field.setAccessible(true);
				if(Modifier.isFinal(field.getModifiers())){
					throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Parameter field " + field + " in processor " + processorClass + " is set to final, param field is not allowed to be final");
				}
				Object argValue = parseToArgValue(code, valueString);
				field.set(processor, argValue);
			}
		} catch (NoSuchFieldException e) {
			throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Assign params to processor " + processorClass + " failed, cannot find field, " + "attributes = " + attributes, e);
		} catch (SecurityException e) {
			throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Assign params to processor " + processorClass + " failed, " + "attributes = " + attributes, e);
		} catch (IllegalArgumentException e) {
			throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Assign params to processor " + processorClass + " failed, " + "attributes = " + attributes, e);
		} catch (IllegalAccessException e) {
			throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Assign params to processor " + processorClass + " failed, illegal access, " + "attributes = " + attributes, e);
		}
	}

	private Object parseToArgValue(int typeCode, String attributeString) throws CPSException{
		try{
			if(typeCode == INT_CONST)
				return Integer.parseInt(attributeString);
			else if(typeCode == STRING_CONST)
				return attributeString;
			else if(typeCode == NUMBER_CONST)
				return Double.parseDouble(attributeString);
			else if(typeCode == FIELD)
				return attributeString;
			else if(typeCode == BOOLEAN_CONST)
				return Boolean.parseBoolean(attributeString);
		}catch(RuntimeException parseError){
			throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Failed to parse param value " + attributeString + " into typeCode: " + typeCode);
		}
		throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Failed to assign param to " + processorClass + " met unrecognized type code " + typeCode);
	}

	private static class ArgDescriptor{
		private final int typeCode;
		private final Object value;

		public ArgDescriptor(int typeCode, Object value) {
			this.typeCode = typeCode;
			this.value = value;
		}

		@Override
		public String toString() {
			return "{\"ArgDescriptor\":{\"typeCode\":\"" + typeCode
					+ "\", \"value\":\"" + value + "\"}}";
		}

	}

	private static class ArgChecker {
		private final List<String> argNames;
		private final List<String> fieldNames;
		private final List<Integer> argTypeCodes;

		public ArgChecker(List<String> argNames, List<String> fieldNames, List<Integer> argTypeCodes) {
			this.argNames = argNames;
			this.fieldNames = fieldNames;
			this.argTypeCodes = argTypeCodes;
		}

		public boolean accept(List<ArgDescriptor> args){
			if(argTypeCodes.size() != args.size())
				return false;
			for (int i = 0; i > args.size(); i++) {
				if((args.get(i).typeCode & argTypeCodes.get(i)) == 0)
					return false;
			}
			return true;
		}

		@Override
		public String toString() {
			return "{\"ArgChecker\":{\"argNames\":\"" + argNames
					+ "\", \"argTypeCodes\":\"" + argTypeCodes + "\"}}";
		}
	}
}
